-
Notifications
You must be signed in to change notification settings - Fork 54
Add custom token support to exposePort for stable preview URLs #329
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Allows users to pass a custom token to exposePort() to maintain consistent preview URLs across deployments and container restarts. This solves the issue of URLs breaking when sandboxes are restarted during deployments, enabling stable URL sharing in production. Token validation ensures 16-character length and URL-safe characters (lowercase letters, numbers, hyphens, underscores) matching the existing routing pattern. Backward compatible - tokens are optional and auto-generated if not provided. Includes comprehensive unit tests for validation logic and custom token usage.
Update token validation to accept variable-length tokens (1-63 chars) instead of fixed 16-character requirement. This provides more flexibility for users to choose meaningful token names while maintaining DNS subdomain compatibility. Changes: - Updated validateCustomToken() to check 1-63 char range - Updated request-handler regex to accept variable-length tokens - Updated all documentation and JSDoc comments - Updated unit tests to cover variable-length tokens - Updated changeset with new token length requirements
🦋 Changeset detectedLatest commit: 334ce49 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Add documentation for the new custom token parameter in exposePort() that enables stable preview URLs across container restarts and deployments. Updated files: - API reference (ports.mdx): Added token parameter documentation with examples - Concepts (preview-urls.mdx): Updated URL format and added URL stability section - Guide (expose-services.mdx): Added practical examples and use case guidance Related PR: cloudflare/sandbox-sdk#329
commit: |
🐳 Docker Images PublishedDefault: FROM cloudflare/sandbox:0.0.0-pr-329-92798c8With Python: FROM cloudflare/sandbox:0.0.0-pr-329-92798c8-pythonWith OpenCode: FROM cloudflare/sandbox:0.0.0-pr-329-92798c8-opencodeVersion: Use the 📦 Standalone BinaryFor arbitrary Dockerfiles: COPY --from=cloudflare/sandbox:0.0.0-pr-329-92798c8 /container-server/sandbox /sandbox
ENTRYPOINT ["/sandbox"]Download via GitHub CLI: gh run download 20972927899 -n sandbox-binaryExtract from Docker: docker run --rm cloudflare/sandbox:0.0.0-pr-329-92798c8 cat /container-server/sandbox > sandbox && chmod +x sandbox |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Claude Code Review
Critical Issue: Validation Ordering Bug
Location: packages/sandbox/src/sandbox.ts:2133-2151
Token validation happens after the container API call. Invalid tokens are sent to the container before validation runs:
// Line 2133: Container called FIRST
await this.client.ports.exposePort(
port,
sessionId,
options?.name,
options?.token // Could be invalid
);
// Line 2151: Validation SECOND (too late)
if (options.token) {
this.validateCustomToken(options.token);
token = options.token;
}If a user passes token: "INVALID@#$", it reaches the container unchecked. While validation eventually throws, the container has already processed the request.
Move validation before the container call:
// Validate token FIRST
let token: string;
if (options.token) {
this.validateCustomToken(options.token);
token = options.token;
} else {
token = this.generatePortToken();
}
const sessionId = await this.ensureDefaultSession();
await this.client.ports.exposePort(
port,
sessionId,
options?.name,
options.token
);Missing Test Coverage
Three critical integration scenarios aren't tested:
- 63-character boundary: Tests reject 64 chars but don't verify 63 chars works (packages/sandbox/tests/sandbox.test.ts:293)
- Token storage: No test verifies custom tokens are stored correctly, unlike auto-generated tokens (line 340-346)
- Request routing: All request-handler tests use 16-char tokens; none test the regex change from
{16}to+with variable lengths (packages/sandbox/tests/request-handler.test.ts)
Add tests that verify custom tokens flow through correctly, not just that bad tokens are rejected.
Verdict
Fix the validation ordering before merge. The test gaps should be addressed but aren't blocking.
| let token: string; | ||
| if (options.token) { | ||
| // Validate custom token | ||
| this.validateCustomToken(options.token); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validation happens after the container API call at line 2133. Move the entire token generation/validation block (lines 2147-2155) before the client.ports.exposePort() call to validate before side effects.
Update custom token validation to enforce a maximum of 16 characters to match the auto-generated token length and ensure consistency. Changes: - Updated validateCustomToken() to check 1-16 char range (was 1-63) - Updated all JSDoc comments to reflect 1-16 char requirement - Updated interface documentation in ExposePortRequest - Updated unit tests to verify 16-char maximum - Updated changeset with corrected token length
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Claude Code Review
The implementation is well-structured with good validation logic and test coverage. However, there's one critical security issue that must be addressed before merging.
Critical Issue: Token Collision
Security Risk (10/10): The code doesn't prevent multiple ports from using the same custom token. This creates a security vulnerability where users could potentially access wrong ports or hijack preview URLs.
// Currently allowed - creates security issue:
await sandbox.exposePort(8080, { hostname: 'example.com', token: 'mytoken' });
await sandbox.exposePort(8081, { hostname: 'example.com', token: 'mytoken' });
// Both URLs use same token - which port gets accessed?Required fix: Before storing a custom token (sandbox.ts:2157), check if it's already in use by another port:
if (options.token) {
const existingTokens = tokens || {};
const existingPort = Object.entries(existingTokens).find(
([p, t]) => t === options.token && p !== port.toString()
);
if (existingPort) {
throw new SecurityError(
`Token '${options.token}' is already in use by port ${existingPort[0]}.`
);
}
token = options.token;
}Required test: Add to sandbox.test.ts:
it('should reject exposing different ports with same custom token', async () => {
await sandbox.exposePort(8080, { hostname: 'example.com', token: 'shared' });
await expect(
sandbox.exposePort(8081, { hostname: 'example.com', token: 'shared' })
).rejects.toThrow(/already in use/);
});Other Issues
Token persistence testing: Missing E2E test for token survival across DO hibernation. This is critical for production stability - if tokens don't persist, preview URLs break after sleep/wake cycles.
Regex edge cases: The URL pattern regex needs tests for consecutive separators (abc--def, test__123) to ensure they parse correctly.
What Looks Good
- Comprehensive input validation with clear error messages
- Proper SecurityError usage
- Type consistency across SDK and shared packages
- Good unit test coverage for validation rules
- Well-documented with JSDoc and examples
- Changeset follows project standards
Verdict: Fix the token collision issue, then this is ready to merge.
packages/sandbox/src/sandbox.ts
Outdated
| token = this.generatePortToken(); | ||
| } | ||
|
|
||
| // Store token for this port (storage is protected by input gates) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Security issue: Need to check if the custom token is already in use by another port before storing. Currently allows token collision which could let users access wrong ports.
Add before line 2157:
if (options.token) {
const existingPort = Object.entries(tokens).find(
([p, t]) => t === options.token && p !== port.toString()
);
if (existingPort) {
throw new SecurityError(
`Token '${options.token}' is already in use by port ${existingPort[0]}.`
);
}
}| function extractSandboxRoute(url: URL): RouteInfo | null { | ||
| // Parse subdomain pattern: port-sandboxId-token.domain (tokens mandatory) | ||
| // Token is always exactly 16 chars (generated by generatePortToken) | ||
| // Token can be any length (1-63 chars) containing only [a-z0-9_-] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment says "1-63 chars" but validateCustomToken limits to 16 chars. The regex accepts up to 63 to be forward-compatible with DNS limits, which is fine, but the comment could be clearer: "Token can be 1-16 chars (SDK validates) or up to 63 chars (DNS limit)"
| }) | ||
| ); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing critical test case: token collision prevention. Add:
it('should reject exposing different ports with same custom token', async () => {
await sandbox.exposePort(8080, { hostname: 'example.com', token: 'shared' });
await expect(
sandbox.exposePort(8081, { hostname: 'example.com', token: 'shared' })
).rejects.toThrow(/already in use/);
});…prevention Critical fixes: - Move token validation before container API call to prevent invalid tokens from reaching the container - Add token collision check to prevent multiple ports using same token - This fixes security vulnerability where wrong ports could be accessed Additional improvements: - Add test for token collision prevention - Add test for custom token storage verification - Add test for re-exposing same port with same token (valid scenario) - Update comment in request-handler.ts for clarity on token length All feedback from Claude Code Review has been addressed.
Empty string tokens were bypassing validation because the truthiness check treated '' as falsy. Changed to explicit undefined check. Also removed token from container API since the container never used it, consolidated verbose unit tests into focused ones, and added E2E test coverage for custom token port exposure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OpenCode Review
This is a well-implemented feature that adds custom token support for stable preview URLs. The implementation is thoughtful with proper validation, collision detection, and security considerations.
Strengths:
- Proper input validation with helpful error messages
- Token collision prevention across ports
- Good test coverage for edge cases
- Maintains backward compatibility
- URL parsing updated to handle variable token lengths
- Excellent changeset with clear examples
Minor Issues to Address:
-
Type inconsistency:
ExposePortRequesttype moved from port-client.ts to shared/request-types.ts but doesn't include the newtokenfield. The container layer won't receive custom tokens. -
Potential race condition: Between lines 2172-2181, there's a gap where another request could claim the same custom token. Consider checking collision after storage read and before container call.
-
Documentation: Request handler comment still mentions "exactly 16 chars" but should reflect "1-16 chars" for custom tokens.
Overall this looks good to merge once the type definition is updated to include the token field in the container API.
Verdict: A few small issues to address first.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OpenCode Review
After reviewing the implementation, this is a well-architected feature that adds custom token support for stable preview URLs.
Strengths:
- Proper input validation with helpful error messages
- Token collision prevention across ports
- Good test coverage for edge cases
- Maintains backward compatibility
- URL parsing updated to handle variable token lengths
- Excellent changeset with clear examples
- Clean separation of concerns: DO handles tokens/URLs, container handles port exposure
- Documentation updated correctly in request handler
Architecture is correct: The token field doesn't need to be in ExposePortRequest because the container layer only manages port exposure, while the Durable Object layer handles token-based URL access control. This separation is appropriate.
Looks good to merge.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OpenCode Review
Looks good overall! The custom token feature for stable preview URLs is well-implemented with proper validation and collision detection.
A few issues to address:
- Missing ExposePortRequest field: The interface was moved to
@repo/sharedbut doesn't include thetokenfield, creating a type mismatch between SDK and container - Documentation clarity: The changeset mentions tokens work "across deployments" but they're stored in DO storage which is ephemeral
The architecture follows established patterns and the security considerations (collision detection, input validation) are solid.
| // With custom token - URL stays the same across restarts | ||
| const { url } = await sandbox.exposePort(8080, { | ||
| hostname: 'example.com', | ||
| token: 'my-token-v1' // 1-16 chars: a-z, 0-9, -, _ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The description mentions "stable preview URLs across deployments" but tokens are stored in Durable Object storage, which is ephemeral and cleared when the sandbox sleeps/restarts. This could mislead users about persistence guarantees.
Consider clarifying: "stable preview URLs within a session" or "while the sandbox remains active" to set accurate expectations.
The standard === operator can leak token contents through timing differences. While not practically exploitable over the network, using crypto.subtle.timingSafeEqual follows Cloudflare's recommended pattern for comparing secrets.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OpenCode Review
Looks good! This is a clean implementation that adds the ability to use custom tokens in exposePort() for stable preview URLs across deployments.
The implementation handles all the important edge cases - token validation, collision detection, and maintains backward compatibility. The timing-safe comparison and input gate protection for storage are correctly implemented. The changeset properly describes the user-facing benefit.
One thing to note: this will require documentation updates since the Cloudflare docs currently show the exposePort() method without the new token parameter. The API reference at /sandbox/api/ports/ and the usage examples in /sandbox/guides/expose-services/ should be updated to document the new optional parameter.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OpenCode Review
This PR adds custom token support to exposePort() for stable preview URLs across deployments. The implementation looks solid with proper validation and security considerations.
Good architectural choices:
- Token collision prevention across ports while allowing re-exposure of same port
- Timing-safe token comparison using
crypto.subtle.timingSafeEqual() - Clear validation with descriptive error messages
- Comprehensive test coverage including edge cases
Security implementation is correct:
- Validates token format (1-16 chars, lowercase a-z, 0-9, -, _)
- Prevents token reuse across different ports
- Uses constant-time comparison to prevent timing attacks
The regex change in request-handler.ts makes sense - relaxing from exactly 16 chars to 1+ chars while maintaining DNS compatibility.
Documentation impact: This change should be documented in the Cloudflare docs since it affects the public API and enables new use cases for production deployments.
Looks good to merge.
Adds support for custom tokens in exposePort. - Allows for consistent URLs across sessions